Delphi ASP.NET Portal Programming - Felix John COLIBRI. |
- abstract : presentation, architecture and programming of the Delphi Asp Net Portal. This is a Delphi version of the Microsoft ASP.NET Starter Kit Web Portal showcase
- key words : ASP.NET - Web Portal - Starter Kit
- software used : Windows XP Personal, Cassini Web Server, Delphi 2006, Asp.Net v1.1.4322 or Asp.Net v2.0.50727, Interbase 6 or Interbase 7.5
- hardware used : Pentium 2.800Mhz, 512 M memory, 25 G hard disc
- scope : Windows developers - Delphi 8 to 2006, Turbo Delphi - Interbase 6 or 7.5, Asp.Net 1 or 2
- level : Web developer - Asp.Net developer
- plan :
1 - The Delphi Asp.Net Web Portal 1.1 - Purpose The goal of a Portal is to offer a framework which can be used to build Web pages having the same look an feel and which can be customized using .HTML
pages included in the Portal. We are going to present the Asp.Net Starter Kit portal, in a Delphi version. Here is a snapshot of the basic Starter Kit page:
1.2 - The Asp.Net Starter Kit The Asp.Net Portal comes bundled with a tutorial
module, as well as many .HTML files explaining how to use the portal, from an administrator point of view. There are also .HMTL pages of the source code, which can be displayed by directly looking at the .CS or .ASPX/ASCX files anyway.
This paper will explain how we handled the project.
1.3 - What was changed The basic "Asp.Net Starter Kit" files are - in C# language
- for ASP.Net version 1
- the database is Sql Server, with all SQL scripts included, and the binary file
- the web server is IIS (Internet Information Server)
We basically started off with the Delphi translation made by
Pascal Chapuis, which used - Delphi 8 or Delphi 2005
- either Firebird, or Sql Server. For Firebird, the FBK backup file is provided, for Sql Server the full Sql scripts are included
- the access components are the Firebird Data Provider, or the Sql Data Provider
- the web server is IIS or Cassini
This version was transformed in the following way - we are using Delphi 2006
- the Database is Interbase 6, or 7.5, with the SQL scripts as well as the binary file
- the access component set is the BDP (Borland Data Provider)
- the Web Server is CASSINI
- Asp.Net is version 1 or 2
1.4 - The strategy Here is a summary of our modifications - we renamed many of the files and identifiers
- we created some additional folders
- we removed the Mobile part
- we commented out the security and Asp.Net cache part
1.5 - The File Organization We will use the following folders: And:
- _data: contains the Interbase database file, and the SQL scripts
- _exe: contains the intermediate compilation files (.DCUIL) and our logs (MUST be in the same folder as the .BDSPROJ)
- 1_helpers: the configuration units, the security handling, and some global helper units (litteral strings etc)
- 2_db_components: most of the units making a connection to the database (with the exception of U_C_CONFIGURATION)
- 3_elements: all components and pages other than the main a_asp_portal.ASPX
- the common files: the portal banner, the Module title, the Module ancestor CLASS
- 31_user_components:
- all ASCX Module components
- 311_edit_user: the ASPX Module edition pages
- 32_admin_components
- the ASCX Modules for administering the portal (changing the portal configuration)
- 321_edit_admin the ASPX pages for portal administration
- bin: Asp.Net JIT compiled code. DO NOT change anything there
- Data: the files used by the "About the Portal" tab (presentation and
tutorial about administering the portal)
- images: the .GIF used by the different .ASPX and .ASCX files
- portal_images: our own logo, placed in the banner
Note that there is also a hidden "__history" folders created and used by
Delphi, as well as hidden files with ~nnn~ extensions, all handled by Delphi
1.6 - The detail of the project We will now present - the programming architecture
- the database
- the configuration files
- the pages used by the users
- the administration of the portal
2 - The Overall Web Portal Architecture
As explained in the Delphi Asp.Net Portal architecture paper, the user sees the Portal as several different pages. Each pages includes - a banner
- several tabs, like in a NoteBook, one of them being selected
- several modules
Such pages are called, in Portal parlance, "views". From a programming viewpoint, the views are assembled using the following elements: - the main frame is the a_asp_portal.aspx template, with
- an instance of the a_c_desktop_portal_banner.ascx Asp.Net component
- an .HTML Table with three cells, called panes, where the modules will be stuffed
Here is the organization:
- the a_c_desktop_portal_banner.ascx Asp.Net component contains
- the site icon
- the site title
- links to the home page and the index
- more important, a DataList which will be used for the tabs
The banner looks like this:
- each Module is an .ASCX Asp.Net component which has
- some kind of header. It can be a simple Label, but in most case it is an a_c_desktop_module_title.ascx Asp.Net component with a Label and an
Hiperlink (for calling a Module edit page)
- some content control (a DataGrid, a DataList, or some TextBoxes)
Here is the a_c_quicklinks.ascx Module (no module title) and the
a_c_html_module.ascx Module (with an a_c_desktop_module_title.ascx module title): Since all Modules must manage some identification parameters common to all
Modules, an abstract c_portal_module_control ancestor class has been created with attributes for ModuleId, PortalConfiguration etc. So all Module CLASSes (like TWebUserControlContacts, for instance)
descend from this common CLASS. The HyperLink control placed on the Module title is used do call an edit page associated with the module. For instance, clicking on the "Edit"
hyperlink of an a_c_announcements.ascx will request and display the the a_edit_announcements.aspx page (which contains TextBoxes, Validators, Buttons etc). The Edit page still has a banner (to keep the same look), but
without any tabs. This can be represented like this: A complete view, with initialized tabs and some modules, looks like this:
In addition to the usual Asp.Net code files (.PAS,.ASPX, .ASCX), the Portal also uses configuration and content data files: - the structure files:
- WEB.CONFIG
- a ASPNETPORTAL.CSS Cascading Style Sheet for the font and graphic properties
- all the images (the arrows, the Asp.Net Logo etc)
- x_asp_portal.xml which contains the structure and properties of each view
(basically what Module is contained in each view, and in which Pane it is displayed) and module definitions (the file name of each Module)
- the content files:
- the Interbase database, whith the content of the Modules (the .HTML text of the a_c_html_module.ascx Module, the list of discussions from the a_c_discussion.ascx Module etc)
- an .XML/XSD sample SALES.XML file to show how to include .XML in the a_c_xml.ascx Module
- image (.GIF) wich are displayed in some Modules, mainly in the a_c_html_module.ascx .HTML content
This can be represented like this:
We will now present in more detail - the Interbase Database
- the configuration file
- the User pages
- the Administration pages
3 - The Delphi Web Portal Database 3.1 - The Database Schema The database schema is the following:
Note that - only 9 of the 16 Modules have database content. For the other Modules
- a_c_xml.ascx uses an .XML file
- a_c_image.ascx uses .GIFs
- the other modules manage in memory data
- the portal_useroles is an N-N link table
3.2 - Database Creation We have provided the Sql script which enables the Interbase database creation.
In addition, we have included the .ZIP of the complete Interbase 6 and Interbase 7.5 database.
3.3 - Ado.Net and the ConnectionString We will use Ado.Net to transfer data to and from the Database.
Ado.Net requires a ConnectionString, which contains the detail of the driver, the .Net assembly, the file location etc. This connection string is set in the Web.Config global file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration> <!-- application specific settings --> <appSettings>
<add key="ConnectionString" value="assembly=Borland.Data.Interbase,Version=2.5.0.0,
...ooo... username=sysdba;password=masterkey"/>
<add key="ConnectionOptions" value="waitonlocks=False;commitretain=False;
...ooo... " /> </appSettings> ...ooo... | |
So fetching the connection string will be performed using:
var my_connection_string: String;
my_xxx_connection: xxxpConnection;
my_connection_string:= ConfigurationSettings.AppSettings.Get('ConnectionString');
my_xxx_connection: xxxpConnection.Create(my_connection_string); |
3.4 - The data table access classes To fetch or modify the content of the tables, separate CLASSes have been defined containing the main method for handling the access. For instance, for
handling contacts, the U_C_CONTACTS_DB contains the following CLASS:
Unit u_c_contact_db; interface
uses system.Configuration, system.Data
, Borland.Data.Provider
, Borland.Data.Common ;
type ContactsDB= class
public
constructor Create;
function GetContacts(moduleId: Integer): DataSet;
function GetSingleContact(itemId: Integer): BdpDataReader;
function AddContact(moduleId: Integer; itemId: Integer;
userName: Widestring; name: Widestring; role:
Widestring; email: Widestring; contact1:
Widestring; contact2: Widestring): Integer;
procedure DeleteContact(itemID: Integer);
procedure UpdateContact(moduleId: Integer; itemId:
Integer; userName: Widestring; name: Widestring;
role: Widestring; email: Widestring; contact1:
Widestring; contact2: Widestring);
end; // ContactsDB // |
3.5 - Access to the Database To access those tables, we chose to use the BDP. We had not much choice anyway, since this is the most obvious way to access Interbase from Delphi 2006.
In the basic product, nearly all requests use stored procedures. So our Interbase database contains those stored procedures. Here is the Get_Events stored procedure:
CREATE PROCEDURE PORTAL_GETEVENTS (p_module_id INTEGER)
RETURNS ( pv_item_id INTEGER,
pv_created_by_user VARCHAR(100),
pv_created_date TIMESTAMP,
pv_title VARCHAR(150),
pv_where_when VARCHAR(150),
pv_description VARCHAR(2000),
pv_expire_date TIMESTAMP ) AS
BEGIN FOR SELECT
itemid, title,
createdbyuser, wherewhen,
createddate, title,
expiredate, description
FROM Portal_Events WHERE
(ModuleID = :p_module_id) AND
(ExpireDate > Current_timestamp) INTO
:pv_item_id, :pv_title, :pv_created_by_user,
:pv_where_when, :pv_created_date, :pv_title,
:pv_expire_date, :pv_description DO
SUSPEND; END | Using the Sql Adapter components, this procedure could be called with:
function EventsDB.f_c_get_events(p_module_id: Integer): DataSet;
var l_c_sql_connection: SqlConnection;
l_c_sql_transaction: SqlTransaction;
l_c_sql_parameter1: SqlParameter;
l_c_sql_adapter: SqlDataAdapter;
l_c_dataset: DataSet; begin
l_c_sql_connection:= SqlConnection.Create(k_configuration_string);
l_c_sql_connection.Open;
l_c_sql_transaction:= l_c_sql_connection.BeginTransaction;
l_c_sql_adapter:= SqlDataAdapter.Create('Portal_GetEvents', l_c_sql_connection);
l_c_sql_adapter.SelectCommand.Transaction:= l_c_sql_transaction;
l_c_sql_adapter.SelectCommand.CommandType:= CommandType.StoredProcedure;
l_c_sql_parameter1:= SqlParameter.Create('@ModuleID', SqlDbType.Integer, 4);
l_c_sql_parameter1.Value:= system.object(p_module_id);
l_c_sql_adapter.SelectCommand.Parameters.Add(l_c_sql_parameter1);
l_c_dataset:= DataSet.Create;
l_c_sql_adapter.Fill(l_c_dataset);
result:= l_c_dataset; l_c_sql_transaction.Commit;
l_c_sql_transaction.Dispose; l_c_sql_connection.Close;
end; // f_c_get_events |
With the BPD, it seems that we have to initialize ALL parameters (not only the
INPUT parameters). Adding a parameters is performed with 3 lines (which certainly could be concatenated). In addition starting the request always uses the same calls. So we created the following helper functions: - to start the connection:
const k_connection_string= 'xxx...';
var g_open_bdp_connection_count: Integer= 0;
procedure open_bdp_connection(p_c_bdp_connection: BdpConnection);
begin p_c_bdp_connection.Open();
Inc(g_open_bdp_connection_count); end; // open_bdp_connection
procedure open_portal_bdp_connection(var pv_c_bdp_connection: BdpConnection);
begin
pv_c_bdp_connection:= BdpConnection.Create(k_connection_string);
open_bdp_connection(pv_c_bdp_connection); end; // open_portal_bdp_connection
procedure open_portal_bdp_connection_transaction(
var pv_c_bdp_connection: BdpConnection;
var pv_c_bdp_transaction: BdpTransaction); begin
open_portal_bdp_connection(pv_c_bdp_connection);
if g_open_bdp_connection_count<= 0
then pv_c_bdp_transaction:= pv_c_bdp_connection.Begintransaction
else pv_c_bdp_transaction:= Nil;
end; // open_portal_bdp_connection_transaction | - to initialize the Stored Procedure parameters:
function f_c_add_input_parameter(p_c_bdp_command: BdpCommand;
p_name: String; p_type, p_subtype: BdpType; p_size: Integer;
p_value: System.Object): BdpParameter;
begin
Result:= BdpParameter.Create(p_name, p_type, p_size);
Result.Direction:= ParameterDirection.Input;
Result.Value:= p_value;
p_c_bdp_command.Parameters.Add(Result);
end; // add_input_parameter
function f_c_add_output_parameter(p_c_bdp_command: BdpCommand;
p_name: String; p_type, p_subtype: BdpType;
p_size, p_max_precision: Integer): BdpParameter; begin
Result:= BdpParameter.Create(p_name, p_type, p_size);
Result.MaxPrecision:= p_max_precision;
Result.Direction:= ParameterDirection.Output;
p_c_bdp_command.Parameters.Add(Result);
end; // add_output_parameter | And our Get_Events stored procedure is called with:
function EventsDB.GetEvents(p_module_id: Integer): DataSet;
var l_c_bdp_connection: BdpConnection;
l_c_bdp_transaction: BdpTransaction;
l_c_bdp_dataadapter: BdpDataAdapter;
l_c_dataset: DataSet; begin
open_portal_bdp_connection_transaction(l_c_bdp_connection, l_c_bdp_transaction);
l_c_bdp_dataadapter:= BdpDataAdapter.Create('Portal_GetEvents', l_c_bdp_connection);
l_c_bdp_dataadapter.SelectCommand.Transaction:= l_c_bdp_transaction;
l_c_bdp_dataadapter.SelectCommand.CommandType:= CommandType.StoredProcedure;
f_c_add_input_parameter(l_c_bdp_dataadapter.SelectCommand, 'MODULEID',
BdpType.Int32, BdpType.Int32, 4, System.Object(p_module_id));
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'ITEMID',
BdpType.Int32, BdpType.Int32, 4, 4);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'CREATEDBYUSER',
BdpType.String, BdpType.String, 100, 100);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'CREATEDDATE',
BdpType.DateTime, BdpType.DateTime, 16, 16);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'TITLE',
BdpType.String, BdpType.String, 150, 150);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'WHEREWHEN',
BdpType.String, BdpType.String, 150, 150);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'DESCRIPTION',
BdpType.String, BdpType.String, 2000, 2000);
f_c_add_output_parameter(l_c_bdp_dataadapter.SelectCommand, 'EXPIREDATE',
BdpType.DateTime, BdpType.DateTime, 16, 16);
l_c_dataset:= DataSet.Create;
l_c_bdp_dataadapter.Fill(l_c_dataset);
result:= l_c_dataset; l_c_bdp_transaction.Commit;
l_c_bdp_transaction.Dispose; close_bdp_connection(l_c_bdp_connection);
end; // get_with_stored_procedure | In fact the code in the .ZIP uses routines with optional loging functionalities
(with journal inputs for database connection, connection count, results etc), which helped us debug the database access. Using those debugging functions, we also found out that the BDP Stored
Procedures did not work as expected. At least we could not make them run whenever the procedure returns a DataSet (deleting, adding, updating or retrieveing single lines worked, but FOR SELECT did not work). So for those
cases, we replaced the stored procedure call with a simple SQL Request, which was used to fill the target DataSet. For instance:
function EventsDB.f_c_get_events_2(p_module_id: Integer): DataSet;
const k_request=
' SELECT ItemID, Title, CreatedByUser, WhereWhen, '
+ CreatedDate, Title, ExpireDate, Description'
+ ' FROM Portal_Events'
+ ' WHERE (ModuleID = ?)'; // AND (ExpireDate > Current_timestamp)';
var l_c_bdp_connection: BdpConnection;
l_c_bdp_transaction: BdpTransaction;
l_c_bdp_dataadapter: BdpDataAdapter;
l_c_dataset: DataSet; begin
open_portal_bdp_connection_transaction(l_c_bdp_connection, l_c_bdp_transaction);
l_c_bdp_dataadapter:= BdpDataAdapter.Create(k_request, l_c_bdp_connection);
f_c_add_input_parameter(l_c_bdp_dataadapter.SelectCommand, 'MODULEID',
BdpType.Int32, BdpType.Int32, 4, System.Object(moduleId));
l_c_dataset:= DataSet.Create;
l_c_bdp_dataadapter.Fill(l_c_dataset);
Result:= l_c_dataset; l_c_bdp_transaction.Commit;
l_c_bdp_transaction.Dispose; close_bdp_connection(l_c_bdp_connection);
end; // f_get_with_request_2 |
4 - The Delphi .Net Portal Configuration files 4.1 - Web.Config This file contains the following information: So the main information are
- the Interbase connection string
- the authentication mode (Forms or Windows)
4.2 - The Portal .XML Configuration File 4.2.1 - X_PORTAL_CONFIG.XML
Here is a view of the X_PORTAL_CONFIG.XML file (partial display): Basically, this file tells us two things - how Modules are incorporated in the Views
- the parameters of each Module
The link between the two is the "ModuleDefId"
The basic structure of this .XML can be represented by this free style BNF-like definition:
config.xml= header <site_configuration ...> .
header= <?XML ...> .
site_configuration= <SITECONFIGURATION global tabs module_definitions ...> .
global= <GLOBAL ...> .
tabs= { <TAB { module } ...> } .
module= <MODULE [ settings ] ...> .
settings = <SETTINGS { setting } ...> .
setting = <SETTING ...> .
module_definitions= { <MODULEDEFINITION ...> } . |
where - <GLOBAL > contains the main title and a global edition parameter
- <TAB > describes one view: it corresponds to one of the banner "tab" (like the tabs of a notebook), and when this tab is selected, the modules
specified within the <TAB> tag will be displayed
- <MODULE > describes each module of the view: which module, at which position etc
- <SETTINGS > for some modules, there are additional parameters, which
are presented in this tag
- <MODULEDEFINITION > describes each available modules (a module for discussions, one for links, one for .HTML text etc)
If we display the same information removing all .XML attribute keys, we get:
1 Home All Users; 1
1 QuickLinks Admins; 8 LeftPane 0 1 false
2 Welcome to the Portal Admins; 5 ContentPane 0 1 true
3 News and Features Admins; 1 ContentPane 0 2 true
4 Upcoming Events Admins; 4 ContentPane 0 3 true
5 This Weeks Special Admins; 5 RightPane 0 1 false
6 Top Movers Admins; 9 RightPane 0 2 false
2 Employee Info All Users; 3
7 Spy Diary Admins; 5 LeftPane 0 1 false
8 HR Benefits Admins; 1 ContentPane 0 1 true
9 Employee Contact Inform. Admins; 2 ContentPane 0 2 true
10 New Employee Documentation Admins; 10 ContentPane 0 3 false
3 Product Info All Users; 5
11 Spy Diary Admins; 5 LeftPane 0 1 false
12 Competition: TradeCraft Admins; 1 ContentPane 0 1 true
13 Competition: Surveillance Admins; 1 ContentPane 0 2 true
14 Competition: Protection Admins; 1 ContentPane 0 3 true
15 Night Vision Goggles Admins; 6 RightPane 0 1 false
16 Competitors to Watch Admins; 8 RightPane 0 2 false
4 Discussions All Users; 7
17 Spy Diary Admins; 5 LeftPane 0 1 false
18 TradeCraft Techniques and Admins; 3 ContentPane 0 1 false
19 Recipes From the Field Admins; 3 ContentPane 0 2 false
20 GoodReads Admins; 10 ContentPane 0 3 false
5 About the Portal All Users; 9
21 Quick Links Admins; 8 LeftPane 0 1 false
22 About the Portal Starter Admins; 5 ContentPane 0 1 true
23 Portal Tabs Admins; 5 ContentPane 0 2 false
24 Portal Modules Admins 5 ContentPane 0 3 false
25 Managing the Portal Admins; 5 ContentPane 0 4 false
26 Managing Portal Layout Admins; 5 ContentPane 0 5 false
27 Managing User Security Admins; 5 ContentPane 0 6 false
6 Admin Admins; 11
28 Module Definitions Admins; 11 RightPane 0 1 false
29 Site Settings Admins; 14 ContentPane 0 1 false
30 Tabs Admins; 13 ContentPane 0 2 false
31 Security Roles Admins; 12 ContentPane 0 3 false
32 Manage Users Admins; 15 ContentPane 0 4 false
Announcements 5_module_components/a_c_announcements.ascx 1
Contacts 5_module_components/a_c_contacts.ascx 2
Discussion 5_module_components/a_c_discussion.ascx 3
Events 5_module_components/a_c_events.ascx 4
Html Document 5_module_components/a_c_html_module.ascx 5
Image 5_module_components/a_c_image_module.ascx 6
Links 5_module_components/a_c_links.ascx 7
QuickLinks 5_module_components/a_c_quick_links.ascx 8
XML/XSL 5_module_components/a_c_xml_module.ascx 9
Documents 5_module_components/a_c_document.ascx 10
Module Types (Admin) 6_admin_components/a_c_module_definitions.ascx 11
Roles (Admin) 6_admin_components/a_c_roles.ascx 12
Tabs (Admin) 6_admin_components/a_c_tabs.ascx 13
Site Settings (Admin) 6_admin_components/a_c_site_settings.ascx 14
Manage Users (Admin) 6_admin_components/a_c_users.ascx 15
|
4.2.2 - U_C_PORTAL_CONFIGURATION.PAS On the code side, this .XML file is managed by the CLASSes contained in U_C_PORTAL_CONFIGURATION.PAS. This file was automatically generated using
Babel_Code (a .CS to .PAS utility), from the original CONFIGURATION.CS file. This is a huge file (40 K for the .CS) and the Delphi counterpart has a 700 line INTERFACE.
This INTERFACE contains a main c_site_configuration CLASS modeling the whole .XML file, with many subclasses for each part of the .XML structure: - GlobalRow for <Global PortalId="0" ...>
- TabRow for <Tab TabId="1" TabName="Home" ...>
- ModuleRow for <Module ModuleId="1" ModuleTitle="QuickLinks" ...>
and with intermediate CLASSes for the structure (lists of tabs, lists of
modulerow), which are called table_xxx, since they will be DataTable descendents. For the modules, for instance, we have:
ModuleDataTable= class(DataTable, System.Collections.IEnumerable)
... | The base c_site_configuration CLASS is essentially used by the U_C_CONFIGURATION's CLASSes (and also by U_C_SECURITY and U_MODULE_DEFINITIONS)
4.2.3 - U_C_CONFIGURATION.PAS The configuration is then managed by several CLASSES contained in U_C_CONFIGURATION.PAS The CLASSes of this unit also manage the configuration of the portal, but are
not so tightly coupled with the .XML file. The information is retrieve from the .XML using the c_site_configuration CLASS, but it is massaged in several ways which are more easy to use in the different parts of the code.
The CLASSes found in this unit are: - c_module_info: allows to sort the Modules by ModuleOrder
- c_module_item: encapsulation of the Module attributes. Mainly used by the administration pages
- c_configuration: creates and modifies the basic c_site_configuration CLASS
- c_tab_info= class: represents the Tab array
- c_tab_strip_details: represent a single tab
- c_tab_item: the main tab properties
- c_portal_info: the main class used to get the configuration parameters
Note that - we replaced the original "_setting" denomination with "_info", since
<SETTINGS> is used in the .XML file to specify some MODULE sub part
4.2.4 - Loading and Using the Configuration The information contained in the .XML is loaded during the
TGlobal.Application_BeginRequest (GLOBALS.PAS) event. This is one of the the very first event of each Asp.Net request, and it is used here to fetch the .XML and load it into an in-memory DataSet. A reference to this Dataset is included
in the HTTP Context, which is available from nearly everywhere in Asp.Net. Here is the DataSet creation:
var l_tab_index, l_tab_id: integer;
l_c_portal_info: c_portal_info;
if assigned(Request.Params['TabIndex'])
then l_tab_index:= Int32.Parse(Request.Params['TabIndex'])
else l_tab_index:= 0;
if assigned(Request.Params['TabId'])
then l_tab_id:= Int32.Parse(Request.Params['TabId'])
else l_tab_id:= 1;
l_c_portal_info:= c_portal_info.Create(l_tab_index, l_tab_id);
Context.Items.Add('portal_info_key', l_c_portal_info);
HttpContext.Current.Items.Add('site_configuration_key',
u_c_configuration.c_configuration.f_c_site_configuration()); | and:
- for the first request, there are no TabIndex, TabId parameters, so the default will be used (TabIndex=0, TabId=1, which is the main view)
- whenever the USER clicks on a Tab, the parameters of this tab will be
appended to the request, and the relevant configuration will be retrieved
In both cases, the configuration is now in the HTTP Context, which travels thru all the layers of the Asp.Net HTTP Pipeline, and will be retrieved using this context:
- the 'portal_info_key' will retrieve the c_portal_info CLASS
- the 'site_configuration_key' will retrieve the c_site_configuration CLASS (mainly in U_C_CONFIGURATION.PAS, and U_C_SECURITY.PAS)
For instance, to set the portal name in the banner (A_C_PORTAL_BANNER.PAS):
procedure TDesktopPortalBanner.Page_Load(sender: System.Object ;
e: System.EventArgs);
var l_c_portal_info: c_portal_info; begin
l_c_portal_info:=
c_portal_info(HttpContext.Current.Items['portal_info_key']);
siteName.Text:= l_c_portal_info.PortalName; |
5 - The User Files We present here the elements that the normal user (not an administrator) will see. They are: - the Modules with their optional title
- the Module Edit pages
- the banner with the tabs
5.1 - The Modules The Modules are Asp.Net components, which are created and placed on the View's Panes. The module is made of a title and a content.
Here is the A_C_CONTACTS.ASCX in the Delphi IDE (the top third of the snapshot is the Delphi IDE part. Only the central white rectangle is the Module control):
and the same when loaded on the default page (the purple color comes from our CenterPane coloring): In the present case, the module contains
- a title custom component, which is an instance of the TDesktopModuleTitle Asp.Net component
- the content of the Module (a DataGrid in this case)
This organization is not mandatory, but frequent. In fact, the Portal contains different Module types, to present how to handle different kind of informations: .HTML content, .XML content, .GIF images etc.
5.1.1 - The Module component ancestor All Module components (user and administration) descend from a basic Asp.Net component which is used to handle the common attributes:
unit u_c_desktop_controls; interface
uses ... ; type c_portal_module_control=
class(UserControl)
private
_moduleConfiguration: c_module_info;
_isEditable: integer;
_portalId: integer;
_settings: Hashtable;
function GetModuleId: integer;
function GetIsEditable: boolean;
function GetSettings: Hashtable;
published
property ModuleId: integer read GetModuleId;
property PortalId: integer read _PortalId write _PortalId;
property IsEditable: boolean read GetIsEditable;
property ModuleConfiguration:
c_module_info read _moduleConfiguration
write _moduleConfiguration;
property Settings: Hashtable read GetSettings;
// ...
end; // c_portal_module_control ====
unit a_c_contacts; interface uses ...
, u_c_desktop_controls; type TWebUserControlContacts=
class(c_portal_module_control)
strict private
procedure InitializeComponent;
strict private
procedure Page_Load(sender: System.Object ;
e: System.EventArgs);
strict protected
myDataGrid: System.Web.UI.WebControls.DataGrid;
procedure OnInit(e: System.EventArgs); override;
end; // TWebUserControlContacts |
5.1.2 - The Module Title Most Module component use an instance of the a_c_desktop_module_title.ascx Module title Asp.Net component. Here is a snapshot of this component displayed in the Delphi IDE:
This component contains - the ModuleTitle Label
- the EditButton Hyperlink, which will be initialized to load the Module
Edit .Aspx page
5.1.3 - Contacts.ascx Here is the Contacts.Ascx template file
- before the selected part is the .ASPx header
- the selected part is the Module Title component (with its EditText title and EditUrl link for calling the Module edition a_edit_contacts.aspx page)
- after the selected part is the DataGrid
On the action side, the a_c_contacts.TWebUserControlContacts.Page_Load event will fetch the events and fill the DataGrid :
procedure TWebUserControlContacts.Page_Load(sender: System.Object ;
e: System.EventArgs);
var l_c_contact_db: ContactsDB;
l_c_dataset: DataSet; begin
l_c_contact_db:= ContactsDB.Create();
l_c_dataset:= l_c_contact_db.GetContacts(ModuleId);
if l_c_dataset.Tables.Count> 0
then begin
myDataGrid.DataSource:= l_c_contact_db.GetContacts(ModuleId);
myDataGrid.DataBind(); end;
end; // Page_Load |
5.2 - The Portal Banner
Here is a snapshot of the a_c_desktop_portal_banner.ascx component in the Ide (with our debugging colors): and the corresponding .ASCX template is:
<%@ Control Language="c#" AutoEventWireup="false"
CodeBehind="a_c_desktop_portal_banner.pas"
Inherits="a_c_desktop_portal_banner.TDesktopPortalBanner"%>
<table class="HeadBg" cellspacing="0" width="100%" border="0"
bgcolor="#ff99ff"> <tbody>
<tr valign="top">
<td style="WIDTH: 68px; HEIGHT: 50px">
<p align="center">
<img style="WIDTH: 45px; HEIGHT: 40px" hspace="0"
src="k_logo_png" align="middle" vspace="2" height="45">
<font size="1">
<a class="sitelink" href="k_http://www.felix-colibri.com/">
k_F_Colibri </a>
</font> </p>
</td>
<td style="WIDTH: 336px; HEIGHT: 50px">
& nbsp ;
<asp:Label id="siteName" width="310px" height="32px" runat="server"
cssclass="SiteTitle" font-size="20pt">
k_Delphi_Asp.Net_Portal </asp:Label>
</td>
<td style="WIDTH: 464px; HEIGHT: 50px">
<p align="left">
<asp:Label id="WelcomeMessage" width="342px" height="19"
runat="server" forecolor="#eeeeee" backcolor="#C0FFFF">
</asp:Label>
<a class="SiteLink"href="c:\programs\us\web\asp_net\
delphi_asp_portal\prog\a_asp_portal.aspx"> k_Index
</a> </p>
</td> </tr>
</tbody> </table>
<asp:DataList id="tabs" runat="server" cssclass="OtherTabsBg"
itemstyle-borderwidth="1" selecteditemstyle-cssclass="TabBg"
itemstyle-height="25" enableviewstate="false"
repeatdirection="horizontal" bordercolor="White" backcolor="#C0C0FF">
<SelectedItemStyle cssclass="TabBg">
</SelectedItemStyle> <SelectedItemTemplate> & nbsp ;
<span class="SelectedTab">
<%# ((u_c_configuration.c_tab_strip_details) Container.DataItem)
.TabName %> </span> & nbsp ;
</SelectedItemTemplate>
<ItemStyle height="25px" borderwidth="1px">
</ItemStyle> <ItemTemplate> & nbsp ;
<a href='<%=u_c_portal_helpers.TGlobalHelpers
.GetApplicationPath2(Request) %>/a_asp_portal.aspx ?tabindex=<%# Container.ItemIndex %>
&tabid=<%# ((u_c_configuration.c_tab_strip_details)
Container.DataItem).TabId %>' class="OtherTabs">
<%# ((u_c_configuration.c_tab_strip_details)
Container.DataItem).TabName %>
</a> & nbsp ; </ItemTemplate>
</asp:DataList> | | where - at the start is the header
- then the .HTML Table for the logo, the home URL etc
- at the end the DataList for the Tab array
And here is the Page_Load event which fills the Tab array:
procedure TDesktopPortalBanner.Page_Load(sender: System.Object ;
e: System.EventArgs);
var l_c_portal_info: c_portal_info;
l_c_tab_strip_detail: c_tab_strip_details;
l_c_authorized_tab_array: ArrayList;
l_tab_index: integer; begin
l_c_portal_info:= c_portal_info(HttpContext.Current.Items['portal_info_key']);
// -- set the site name
siteName.Text:= l_c_portal_info.PortalName;
// --- If user logged in, customize welcome message
if Request.IsAuthenticated then
begin
// --- if authentication mode is Cookie, provide a logoff link
if (Context.User.Identity.AuthenticationType= 'Forms')
then LogoffLink:= TGlobalHelpers.GetApplicationPath2(Request)+
k_logoff_module_aspx;
// --- initialize welcome, and the logoff link
WelcomeMessage.Text:= (k_welcome_message.Replace('%1s', Context.
User.Identity.Name))+ k_logoff_message.Replace('%1s',
LogoffLink); end;
// -- set the active tab index TabIndex:= 1;
if ShowTabs
then TabIndex:= l_c_portal_info.m_c_active_tab_info.TabIndex
else begin
// --- toggle visibility
tabs.Visible:= FALSE;
exit; end;
// -- Build list of tabs to be shown to user
l_c_authorized_tab_array:= ArrayList.Create();
// -- fill the array and set the selected index
for l_tab_index:= 0 to l_c_portal_info.m_c_desktop_tab_array.Count- 1 do
begin
write_to_asp_net_log('select_tab ? '+ l_tab_index.ToString);
l_c_tab_strip_detail:= c_tab_strip_details(l_c_portal_info.m_c_desktop_tab_array[l_tab_index]);
// -- add to the array if security is ok
if c_portal_security.IsInRoles(l_c_tab_strip_detail.AuthorizedRoles)
then l_c_authorized_tab_array.Add(l_c_tab_strip_detail);
// -- set the selected tab
if l_tab_index= tabIndex
then tabs.SelectedIndex:= l_tab_index;
end; // for l_tab_index
// -- render the Page tab array with authorized tabs
tabs.DataSource:= l_c_authorized_tab_array;
tabs.DataBind(); end; // Page_Load |
5.3 - The Main page The main page a_asp_portal.aspx looks like this: and the initialization of the Modules in the Panes takes place during the
OnInit event (the loading from cache has been removed in our display):
procedure t_asp_portal_form.OnInit(e: EventArgs);
var l_c_portal_info: c_portal_info;
l_c_module_info: c_module_info;
l_c_portal_module_control: c_portal_module_control;
l_c_parent_control: Control;
l_module_index: integer; begin
InitializeComponent; inherited OnInit(e);
// -- Obtain c_portal_info from Current Context
l_c_portal_info:= c_portal_info(HttpContext.Current.Items['portal_info_key']);
// -- Ensure that the visiting user has access to the current page
if not c_portal_security.IsInRoles(l_c_portal_info.m_c_active_tab_info.AuthorizedRoles)
then Response.Redirect(k_access_denied_module_aspx);
// -- Dynamically inject a signin login module into the top left-hand corner
// -- of the home page if the client is not yet authenticated
if not Request.IsAuthenticated and (l_c_portal_info.m_c_active_tab_info.TabIndex= 0)
then begin
Page.FindControl('LeftPane').Controls.Add(Page.LoadControl(k_signin_module_ascx));
Page.FindControl('LeftPane').Visible:= true;
end;
// -- Dynamically Populate the Left, Center and Right pane sections of the portal page
// -- with the modules defined in x_portal_cfg
if l_c_portal_info.m_c_active_tab_info.Modules.Count> 0
then
// -- Loop through each entry in the configuration system for this tab
for l_module_index:= 0 to l_c_portal_info.m_c_active_tab_info.Modules.Count- 1 do
begin
// -- in which parent pane should this module be positioned ?
l_c_module_info:=
c_module_info(l_c_portal_info.m_c_active_tab_info.Modules[l_module_index]);
l_c_parent_control:= Page.FindControl(l_c_module_info.PaneName);
// -- create the user control instance and dynamically
// -- inject it into the page.
l_c_portal_module_control:=
c_portal_module_control(Page.LoadControl(l_c_module_info.DesktopSrc));
l_c_portal_module_control.PortalId:= l_c_portal_info.PortalId;
l_c_portal_module_control.ModuleConfiguration:= l_c_module_info;
// -- add the module to its parent
l_c_parent_control.Controls.Add(l_c_portal_module_control);
// -- Dynamically inject separator break between portal modules
l_c_parent_control.Controls.Add(LiteralControl.Create('<br>'));
l_c_parent_control.Visible:= true;
end; // for l_module_index
end; // OnInit |
5.4 - The Global.Pas File
The Global.BeginRequest, which has already been partially presented above, and here is a more complete version (the language initialization removed):
procedure TGlobal.Application_BeginRequest(sender: System.Object ;
e: EventArgs);
var l_tab_index, l_tab_id: integer;
l_c_portal_info: c_portal_info; begin
// -- default values l_tab_index:= 0; l_tab_id:= 1;
// -- Get l_tab_index from querystring
if (assigned(Request.Params['TabIndex']))
then l_tab_index:= Int32.Parse(Request.Params['TabIndex']);
// -- Get l_tab_id from querystring
if (assigned(Request.Params['TabId']))
then l_tab_id:= Int32.Parse(Request.Params['TabId']);
l_c_portal_info:= c_portal_info.Create(l_tab_index, l_tab_id);
Context.Items.Add('portal_info_key', l_c_portal_info);
// -- Retrieve and add the configuration DataSet to the current Context
HttpContext.Current.Items.Add('site_configuration_key',
u_c_configuration.c_configuration.f_c_site_configuration());
end; // Application_BeginRequest |
5.5 - The basic cycle 5.5.1 - The User calls the first time
The basic scenario is the following: | the Web Server waits for incoming requests: |
| the User calls the default a_asp_portal.aspx file using his Browser |
| the Asp.Net Server gets the request and then - calls the Globals.BeginRequest event to load the configuration and include it in the Http.Context
- the a_asp_portal.aspx.Page_Load event is called. Since there is no ModuleId parameter, the default value of 1, corresponding to the first tab is used. The configuration for this View is grabbed from the
Context, which tells which Modules should be loaded
- each Module is built in turn, with its title and content filled
The corresponding page is sent back the user
|
Here is what the User will get:
In an UML Sequence diagram, this would look like this:
Granted, this is not a bona-fide UML sequence diagram. Usually: - they contain Objets at the top of the diagram (and not file names)
- Grady Booch included neither any object-container, like we used for modules, nor any multiplicity (modules again)
- when an .ASPX contains an Asp.Net custom control, there is no visible method
call to instanciate the component class (at the developer level). This is performed inside Asp.Net
- and, or course, there are no fancy colors !
We could have presented the standard events called upon each Asp.Net Request:
BeginRequest(), AuthenticateRequest(), AuthorizeRequest(), ... all the way up to PreSendRequest(). But this would have been too detailed, given the number of components involved here.
So we have presented this diagram instead, hoping that it somehow still conveys the processing involved at the Web Server level.
5.5.2 - The User selects another Tab
When the User has received his .HTML page, he can click on a tab or on any hyperlink. Selecting any Tab, this will trigger the "OnClick" event of this item. This handling is located in the a_c_desktop_portal_banner.ascx template. The text of
the .ASCX has been presented above, but the real processing has been drowned in the formating flood. Here is a more focused version: and:
These snippets display the TEMPLATE code, which is located on the Server. When the Server receives a request, it uses the template and the .PAS code to build the true .HTML page that is sent over to the User. In our case, after
receiving the default page, we saved it on disc, and here is a partial listing of this default view: So when the User clicks on a tab, it is the hilighted link that is clicked,
and this link contains exactly the .HTML request for the page with the View parameters.
The scenario looks like this |
the User clicks a Tab, which sends the request to the Server | |
the Server builds the page and returns the .HTML to the User: |
And here is what the user will see after clicking the "Product Info" tab:
6 - The Administration Files Those Modules are used to let users with Administrator rights modify the
x_portal_cfg.xml file, which in fact reorganizes the presentation of the Views. We will not present those Modules in this paper
7 - What is missing - Lessons learned
7.1 - What is missing in this downloadable version You can download the .ZIP of the project. In those files, we removed the following parts
- all Mobile files were removed. We do not use a mobile so we could not test those parts. The mobile parameters are however still present in the configuration files and CLASSes
- we commented out the security part. When we started to use the Portal, we could not access to anything, and we had to use a Data Explorer to understand the password and registering system. In addition, having to fight
with passwords during all trials tests was a pain in the neck. So we commented out the security checks
- we removed the security (logging, roles etc) and the Asp.Net caching
In addition, our purpose being the presentation of the overall architecture, we did not test all features. For instance: - we did not check the User editing
- we did not check the administration Modules
In the production version of the portal, the security and caching are reinstalled.
7.2 - The Renaming and Prefixing In order to be sure that we understood the interactions between all the parts,
we performed, as ususal, a massive renaming of files and variables. Lets take an example: what is Text ? Text is: - the name of a file Type (the ascii buffered FILE)
- the name of a tWinComponent PROPERTY (the Win32 WindowString equivalent), which becomes Caption or Text, and is available even in tForms, even if it is not surfaced in the Object Inspector
But since Text is a predefined word (and not a KEYword), you can name any identifier of your choice Text: a CONST, a TYPE, a VAR, and also parameters, procedures, functions, methods etc. But because of the possible
conflicts with the previous significations, WE will NEVER use "text" as the name of one of OUR identifiers. Our usual solution is to use a prefixing scheme, much similar to Charles
SYMONY's Hungarian notation (a "T" prefix for types, a "wm_" prefix for Windows Messages etc), which is the Alsacian Notation, which prefixes types with T_, constants with K_, globals with G_, locals with
L_, classes with C_ etc. In this case: - we renamed all files with the U_ (units) or A_ (ASPX or .ASCX) prefix. The ONLY files we could not change were GLOBALS and WEB.CONFIG, because this seems to be hard coded in the ASP.NET system.
- many (not all) of the CLASSes were renamed C_xxx, and many (not all) of the local variables were prefixed with L_xxx
- we renamed ALL litteral strings in the .ASPX and .ASCX files with the K_ prefix
Renaming of files and identifiers has the additional huge benefit of narrowing down the results of any "find in file" request (l_c_announcement_db is more discriminating than announcements) as well as allowing further automated
renaming for internationalization, for instance (renaming everywhere k_Name is easier and less error prone than renaming Name).
7.3 - What was added We did add
- we added file logging capabilities, in order to find bugs and trace the different steps of the execution.
- we added colors and separators to the main template, in order to better visualize the View contents of the pages
The logging and coloring have been removed in the production version.
7.4 - Our biggest hurdles We were mainly slown down by the following points
- the switch from FireBird / Sql Server to Interbase. The whole scripts had to be recovered and adapted (they are in the .ZIPs)
- with the scripts, the loading of the .HTML blobs also caused some problems.
We first forgot to remove the quotes. Then the reengineered versions had some .HTML encoding which had to be undone. And an obscure bug in our blob loader removed the first character of the blob, which turned out to be
catastrophic when the first character is the "<" start of an .HMTL tag (when erroneous .HMTL text is injected in the page, the whole .HTML Table structure can be thrown off balance)
- the use of the BDP access layer. Some parts would not work (Stored Procedures with SELECT FOR constructs)
- the use of the Interbase Delphi version also limits the Portal navigation,
since the user count can very quickly get beyond the 4 user limit. So there should be better check on closing the Database after retrieving the rows
- Cassini also had some synchronization errors: when the Delphi compilation
is complex (many modifications), sometimes Cassini is launched after the Browser request. They are some advices on Tamaracka, but we did not implement them in this debug version. In addition, closing the .HTML and
Cassini sometimes blocked the system. So we systematically display the Task Manager and place its icon in the task bar in order to close those files only when all Asp.Net threading activity has stopped.
7.5 - Basic advice
If we had some advice to developers wanting to change the code
7.6 - Asp.Net development Tools Here are some of the tools we used: - first, in order first to convert the Database to Interbase, we used many
Database tools (script extractor, script executer, blob loader, UDF tester, SP tester)
- the Asp.Net logging unit was already mentioned
- ASCII "find in files" (searching in SEVERAL EXTENSION files at the same time), as well as filtering and replacement tools (listing all tag attributes, finding all litteral strings etc)
- different syntactic color viewers and pretty printers for the ASPX, ASCX, XML and CONFIG files
Like for any important project (an accounting system, a text editor, a vector
graphic tool), much of the value is derived from the additional applications which help develop the project and extend its functionality. Many of our tools have been published with source (the log, the .HTML color
display etc). Just look at the site map or use google to locate them
8 - Download the Sources Here are the source code files:
The .ZIP file(s) contain:
- the main program (.BDSPROJ, .DPR, .RES, .CSS, .ASAX), the main form (.PAS, .ASPX), and any other auxiliary form and files (.XML)
- any .TXT for parameters, samples, test data
- all units (.PAS) for units and templates (.ASCX, .ASPX)
- the SQL scripts and binaries
Those .ZIP - are self-contained: you will not need any other product
- will not modify your PC in any way (no registry changes, no hidden file, no .BAT etc).
To use the .ZIP: For using Interbase 7.5, unzip delphi_asp_net_portal_75_ibx.zip AND change the
connection string and database name in WebConfig. To remove the .ZIP simply delete the folder.
As usual:
- please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs, broken links or had some problem downloading the file. Resulting corrections will
be helpful for other readers
- we welcome any comment, criticism, enhancement, other sources or reference suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
- or more simply, enter your (anonymous or with your e-mail if you want an answer) comments below and clic the "send" button
- and if you liked this article, talk about this site to your fellow developpers, add a link to your links page ou mention our articles in
your blog or newsgroup posts when relevant. That's the way we operate: the more traffic and Google references we get, the more articles we will write.
9 - References
- The Asp.Net Starter Kit The Asp.Net Starter Kit : the starter kit in action
In January 2007, this URL was no longer valid, and redirected the user to the Dot Net Nuke Site. However the http://www.asp.net/ URL still offers Asp.Net portals of many different makes and sorts.
We recently found out that the Asp.Net version 1 can still be found in the Asp.Net 1.1 archive page, but this could change
- The Microsoft Dowload: the Microsoft "CSSDK" msdn entry
contains the "ASP.NET Portal (CSSDK) Installer v1.0.msi" "Starter Kit" downloadable file (1.751 Meg)
In January 2007, this URL also does not seem to be still valid, but googling around, you might still find the original Starter Kit
- Pascal Chapuis uploaded his Delphi CSSDK Delphi translation of the Starter Kit on Code Central. It can be freely downloaded
from there. This excellent piece of code was the starting point of the Starter Kit version we presented here
- Hungarian Notation: the Hungarian
Notation explained
- on the tool side:
- finally "a word from our sponsor": the Pascal Institute organises each month training courses about Delphi programming. Of concern to an Asp Portal developer: I am the only teacher, so, if you liked this presentation, you should love the courses
In addition we offer support or full development in the area of Delphi, Database management or Web development To contact us, just send an e-mail to fcolibri@felix-colibri.com.
10 - The author Felix John COLIBRI works at the Pascal Institute. Starting with Pascal in 1979, he then became involved with Object
Oriented Programming, Delphi, Sql, Tcp/Ip, Html, UML. Currently, he is mainly active in the area of custom software
development (new projects, maintenance, audits, BDE migration, Delphi Xe_n migrations, refactoring), Delphi Consulting and Delph training. His web site features tutorials, technical papers about programming with full downloadable source
code, and the description and calendar of forthcoming Delphi, FireBird, Tcp/IP, Web Services, OOP / UML, Design Patterns, Unit Testing training sessions.
|